import os, sys, json, argparse, cv2, enum, datetime, uuid, base64
from PIL import Image
from shapely.geometry import Point as SPoint
from shapely.geometry.polygon import Polygon as SPolygon
# from PIL.ImageQt import ImageQt
# from matplotlib import pyplot as plt
from PyQt5.QtCore import (
        QCoreApplication, pyqtSignal, QThread, Qt, QTimer,
        QDate, QSize, QPoint, QRectF, QEvent
        )
from PyQt5.QtWidgets import (
        QApplication, QWidget, QTreeWidget, QTreeWidgetItem,
        QHBoxLayout, QVBoxLayout, QAbstractItemView,
        QLabel, QPushButton, QCheckBox, QSizePolicy,
        QGroupBox, QComboBox, QLineEdit, QDateEdit,
        QFileDialog, QTextBrowser, QProgressBar,
        QGraphicsScene, QGraphicsView, QGraphicsItem, QGraphicsPixmapItem,
        QGraphicsSimpleTextItem, QGraphicsLineItem, QGraphicsRectItem,
        QGraphicsEllipseItem, QGraphicsPathItem, QGraphicsPolygonItem,
        )
from PyQt5.QtGui import (
        QImage, QPixmap, QPainter, QPainterPath,
        QColor, QBrush, QPen, QFont,
        QMouseEvent
        )

# ============================== 日志设置 ===================================bgn
from zjLogger import logger
# ============================== 日志设置 ===================================end

class Act(enum.Enum):
    Invalid = -1
    Annotate = 1
    Measure = 2

class Shape(enum.Enum):
    Invalid = -1
    Point = 1
    Line = 2
    Circle = 3
    Rect = 4
    Polygon = 5
    UnPoint = 6

class Color:
    HColor = Qt.yellow
    LColor = Qt.red

class TagColor(Color):
    HColor = Qt.red
    LColor = Qt.red

class ZJColor(Color):
    HColor = Qt.green
    LColor = Qt.green

class CocoColor(Color):
    HColor = Qt.yellow
    LColor = Qt.red

DCT_COLOR = {
        'Tag': TagColor,
        'ZJ': ZJColor,
        'COCO': CocoColor,
        }

def get_imgs(root):
    lst_imgs = []
    if not os.path.exists(root):
        return lst_imgs
    for d in os.listdir(root):
        if root == '.': sub = d
        else: sub = os.path.join(root, d)
        if os.path.isdir(sub):
            # lst_imgs.extend(get_imgs(sub))
            continue
        elif os.path.isfile(sub):
            name, ext = os.path.splitext(sub)
            if ext in ('.JPG', '.jpg'):
                lst_imgs.append(sub)
        else: pass
    return lst_imgs

class GraphicsSTextItem(QGraphicsSimpleTextItem):
    def __init__(self, x, y, parent=None, text='',
            scale=1.0, fontSize=14, color=Color,
            isEnableMove=False, isEnableSelect=True):
        super().__init__(parent)
        if isEnableSelect: self.setFlag(QGraphicsItem.ItemIsSelectable)
        if isEnableMove: self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setFlag(QGraphicsItem.ItemIsFocusable)
        self.setAcceptHoverEvents(True)
        self.scale = scale
        self.color = color
        self.setHighLight(False)
        self.point = (int(x * self.scale), int(y * self.scale))
        self.setFont(QFont('DejaVuSans', int(self.scale * fontSize)))
        self.setText(text)
        self.setPos(QPoint(*self.point))

    def setHighLight(self, flag):
        if flag: self.setBrush(self.color.HColor)
        else: self.setBrush(self.color.LColor)

    def focusInEvent(self, event):
        self.setHighLight(True)

    def focusOutEvent(self, event):
        self.setHighLight(False)

class GraphicsPathItem(QGraphicsPathItem):
    def __init__(self, parent=None, scale=1.0, color=Color,
            isEnableMove=False):
        super().__init__(parent)
        self.setFlag(QGraphicsItem.ItemIsSelectable)
        if isEnableMove: self.setFlag(QGraphicsItem.ItemIsMovable)
        self.setFlag(QGraphicsItem.ItemIsFocusable)
        self.setAcceptHoverEvents(True)
        self.scale = scale
        self.color = color
        self.setHighLight(False)
        self.lst_points = []
        self._path = QPainterPath()

    def setHighLight(self, flag):
        if flag: self.setPen(self.color.HColor)
        else: self.setPen(self.color.LColor)

    def focusInEvent(self, event):
        self.setHighLight(True)

    def focusOutEvent(self, event):
        self.setHighLight(False)

    def bgnPoint(self, x, y):
        x, y = x * self.scale, y * self.scale
        self.lst_points.append((x, y))
        self._path.moveTo(x, y)

    def addPoint(self, x, y):
        x, y = x * self.scale, y * self.scale
        self.lst_points.append((x, y))
        self._path.lineTo(x, y)
        self.setPath(self._path)

    def endPoint(self, x, y):
        x, y = x * self.scale, y * self.scale
        self.lst_points.append((x, y))
        self._path.lineTo(x, y)
        self._path.lineTo(self.lst_points[0][0], self.lst_points[0][1])
        self.setPath(self._path)

class GraphicsScene(QGraphicsScene):
    def __init__(self, parent=None):
        super().__init__(parent)

class GraphicsView(QGraphicsView):
    loadAnntSignle = pyqtSignal()
    modifyAnntSignle = pyqtSignal()
    modifyItemSignle = pyqtSignal()

    def __init__(self, parent=None, relSize=(800, 800), absSize=(800, 800)):
        super().__init__(parent)

        self.initAttribute()
        self.parent = parent
        self.lbZoom = None
        self.img = None
        self.img_item = None
        self.relSize = relSize
        self.absSize = absSize
        self.size_scale_val = min(self.absSize) / min(self.relSize)
        self.isLMousePress = False
        self.isRMousePress = False
        self.isScrollDrag = False
        self.slideFile = None
        self.anntFile = None
        self.slideImg = None
        self.currItem = None
        self.currPoses = None
        self.curr_annt_type = None
        self.lt_time = None
        self.annt_type = None
        self.tag_type = 'Tag'
        # {'ZJ': {'Pathology': []}, 'Tag': {'tags': {}}}
        self.dct_annts = {'Tag': {}}
        self.initUI()
        self.setStyleSheet('border: 0px; background: transparent')
        self.resize(*self.absSize)

    def initUI(self):
        self.scene_ = GraphicsScene(self)
        self.setScene(self.scene_)
        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.setDragMode(self.currDragMode)
        self.setViewportUpdateMode(QGraphicsView.FullViewportUpdate)
        self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse)
        # self.setTransformationAnchor(QGraphicsView.AnchorViewCenter)

        # brush = QBrush(QColor(192, 192, 192))
        # brush = QBrush(QColor(255, 255, 255))
        # self.setBackgroundBrush(brush)

    def initAttribute(self):
        self.isEnableZoom = True
        self.isExitSave = False
        self.isEnableMove = False
        self.size_scale_val = 1.0
        self.scale_val = 1.0
        self.img_scale_val = 1.0
        self.act = Act.Invalid
        self.shape = Shape.Invalid
        self.currDragMode = QGraphicsView.RubberBandDrag
        self.refreshTime = 0.1

    def setAttribute(self, isEnableZoom=None,
            isExitSave=None, isEnableMove=None):
        if isEnableZoom is not None:
            self.isEnableZoom = isEnableZoom
        if isExitSave is not None:
            self.isExitSave = isExitSave
        if isEnableMove is not None:
            self.isEnableMove = isEnableMove

    def getAttribute(self):
        pass

    def hasImg(self):
        if self.img:
            return True
        return False

    def loadImg(self, image=None):
        if isinstance(image, str):
            if len(image) > 1024: # 可能是base64
                image = base64.b64decode(image)
                self.slideImg = QImage()
                self.slideImg.loadFromData(image)
                self.img = QPixmap(self.slideImg)
            elif os.path.isfile(image): # 图片路径
                self.slideFile = image
                self.slideImg = QImage()
                self.slideImg.load(image)
                self.img = QPixmap.fromImage(self.slideImg)
        elif isinstance(image, bytes):
            self.slideImg = QImage()
            self.slideImg.loadFromData(image)
            self.img = QPixmap(self.slideImg)
        else:
            self.img = None

        if self.img:
            if self.img_item is None:
                self.img_item = QGraphicsPixmapItem()
                self.scene_.addItem(self.img_item)
                self.img_item.annt_type = None
                self.img_item.uuid = None

            self.img_scale_val = min(self.absSize[0]/self.relSize[0], self.absSize[1]/self.relSize[1])
            self.img = self.img.scaled(self.absSize[0], self.absSize[1], Qt.KeepAspectRatio)
            self.img_item.setPixmap(self.img)
            self.autoAdapt()

    def updateAnnt(self, annt_type, dct_annt):
        # annt_type: Tag, ZJ, COCO
        # dct_item_uuid = {item.uuid: item for item in self.scene_.items() if item.annt_type == annt_type}
        dct_item_uuid = {item.uuid: item for item in self.scene_.items()}
        color = DCT_COLOR.get(annt_type, Color)
        if annt_type == 'Tag':
            if dct_annt.get('tags') is None:
                # logger.error('dct_annt have no tags')
                return

            for name, tags in dct_annt['tags'].items():
                if name == 'is_cover': continue
                elif name == 'add_point':
                    for tag in tags:
                        if tag['id'] in dct_item_uuid.keys(): continue
                        item = self.addPoint(tag['point']['x'], tag['point']['y'], color=color)
                        item.annt_type = annt_type
                        item.uuid = tag['id']
                elif name == 'del_point': pass
                elif name == 'del_area':
                    for tag in tags:
                        if tag['id'] in dct_item_uuid.keys(): continue
                        paths = [(p['x'], p['y']) for p in tag['polygon']]
                        item = self.addPolygon(paths, color=color)
                        item.annt_type = annt_type
                        item.uuid = tag['id']
        elif annt_type == 'ZJ':
            for _ in range(1):
                dct_tag = self.dct_annts.get(self.tag_type, {})
                tags = dct_tag.get('tags', {})
                if tags.get('is_cover') is True: # 覆盖ZJ标注
                    for UUID, item in dct_item_uuid.items():
                        if UUID is not None:
                            self.scene_.removeItem(item)
                    break

                if dct_annt.get('Pathology') is None:
                    # logger.error('dct_annt have no Pathology')
                    break

                lst_hide_uuid = []
                for a in tags.get('del_point', []):
                    lst_hide_uuid.append(a['uuid'])

                for a in tags.get('del_area', []):
                    polygon = SPolygon([(p['x'], p['y']) for p in a['polygon']])
                    for annt in dct_annt['Pathology']:
                        label, x, y, w, h, _, _, confidence, paths, attrs = annt
                        point = SPoint(x+w/2, y+h/2)
                        if polygon.contains(point):
                            lst_hide_uuid.append(attrs['UUID'])

                    # 删除Tag类型的point, TODO 如不需要区域删除手动添加的斑点则删除此段代码
                    # --------------------------------------------------- #
                    # if self.dct_annts.get('Tag') is None: continue
                    # dct_tags = self.dct_annts['Tag'].get('tags')
                    # if dct_tags is None: continue
                    # for name, tags in dct_tags.items():
                    #     if name != 'add_point': continue
                    #     lst_del_tag = []
                    #     for tag in tags:
                    #         x, y = tag['point']['x'], tag['point']['y']
                    #         point = SPoint(x, y)
                    #         if polygon.contains(point):
                    #             # 删除多边形区域内的点
                    #             lst_del_tag.append(tag)
                    #             UUID = tag['id']
                    #             item = dct_item_uuid.get(UUID)
                    #             if item is not None:
                    #                 self.scene_.removeItem(item)
                    #     for tag in lst_del_tag:
                    #         tags.remove(tag)
                    # --------------------------------------------------- #

                # 显示需要显示的图元
                for annt in dct_annt['Pathology']:
                    label, x, y, w, h, _, _, confidence, paths, attrs = annt
                    if attrs['UUID'] in lst_hide_uuid: continue
                    if attrs['UUID'] in dct_item_uuid.keys(): continue
                    if paths: # 有多边形则显示多边形
                        item = self.addPolygon(paths, color=color)
                        item.annt_type = annt_type
                        item.uuid = attrs['UUID']
                    else: pass # 显示矩形

                # 删除应该隐藏的图元
                for UUID in lst_hide_uuid:
                    item = dct_item_uuid.get(UUID)
                    if item is not None:
                        self.scene_.removeItem(item)
                self.modifyItemSignle.emit()
        # elif annt_type == 'ZJ':
        #     if dct_annt.get('Pathology') is None:
        #         logger.error('dct_annt have no Pathology')
        #         return
        #
        #     for annt in dct_annt['Pathology']:
        #         label, x, y, w, h, _, _, confidence, paths, attrs = annt
        #         if attrs['UUID'] in lst_item_uuid: continue
        #         if paths: # 有多边形则显示多边形
        #             item = self.addPolygon(paths, color=color)
        #             item.annt_type = annt_type
        #             item.uuid = attrs['UUID']
        #         else: # 显示矩形
        #             pass
        elif annt_type == 'COCO':
            logger.error('{} is not currently supported'.format(annt_type))
        else:
            logger.error('{} is undefined'.format(annt_type))

    def clearSlide(self):
        self.scene_.clear()
        self.img_item = None
        self.img = None
        self.dct_annts = {'Tag': {}}

    def loadAnnt(self, annt=None, annt_type=None):
        if self.img is None: return
        self.annt_type = annt_type
        self.curr_annt_type = annt_type
        if isinstance(annt, str):
            self.anntFile = annt
            if os.path.isfile(annt):
                with open(annt, 'r') as f:
                    self.dct_annts[annt_type] = json.load(f)
                self.updateAnnt(annt_type, self.dct_annts[annt_type])
            else:
                logger.info('{} is not file'.format(annt))
            self.loadAnntSignle.emit()
        elif isinstance(annt, dict):
            self.dct_annts[annt_type] = annt
            self.updateAnnt(annt_type, self.dct_annts[annt_type])
            self.loadAnntSignle.emit()
        else:
            logger.error('{} is undefined'.format(annt))

    def saveAnnt(self):
        # logger.info('saveAnnt')
        if self.anntFile is None: return

        isSave = False
        if self.curr_annt_type == 'Tag':
            isSave = True
        elif self.curr_annt_type == 'ZJ':
            isSave = True
        elif self.curr_annt_type == 'COCO':
            logger.error('{} is not currently supported'.format(self.curr_annt_type))
            return
        else:
            logger.error('{} is undefined'.format(self.curr_annt_type))
            return

        if isSave:
            with open(self.anntFile, 'w') as f:
                json.dump(self.dct_annts[self.curr_annt_type], f, indent=2)

    def setZoomInfo(self):
        if False and self.isEnableZoom:
            if self.lbZoom is None:
                self.lbZoom = QLabel('', self)
            self.lbZoom.setText(f'缩放: {self.scale_val:.1f}')
            pos = QPoint(self.width()-80, 30)
            self.lbZoom.move(pos * self.size_scale_val)

    def zoom(self, detal):
        if self.img:
            old_scale_val = self.scale_val
            new_scale_val = self.scale_val + detal / 1200
            if 2 >= new_scale_val >= 0.1:
                self.scale_val = new_scale_val
                self.scale(self.scale_val/old_scale_val, self.scale_val/old_scale_val)
                self.setZoomInfo()

    def autoAdapt(self):
        if self.img:
            old_scale_val = self.scale_val
            self.scale_val = min(self.width(), self.height()) / min(self.absSize)
            self.scale(self.scale_val/old_scale_val, self.scale_val/old_scale_val)
            self.setZoomInfo()

    # def resizeEvent(self, event):
    #     super().resizeEvent(event)

    def wheelEvent(self, event):
        if self.isEnableZoom:
            self.zoom(event.angleDelta().y())
            self.setFocus()

    def delAnntByUUID(self, annt_type, dct_annt, UUID):
        isDone = False
        if annt_type == 'Tag':
            for name, tags in dct_annt['tags'].items():
                if isDone: break
                if name == 'is_cover': continue
                for tag in tags:
                    if isDone: break
                    if tag['id'] == UUID:
                        tags.remove(tag)
                        isDone = True
        elif annt_type == 'ZJ':
            for annt in dct_annt['Pathology']:
                if isDone: break
                label, x, y, w, h, _, _, confidence, paths, attrs = annt
                if attrs['UUID'] == UUID:
                    dct_annt['Pathology'].remove(annt)
                    isDone = True
        else: logger.error('{} is not currently supported'.format(annt_type))
        if isDone:
            self.saveAnnt()
            self.modifyAnntSignle.emit()

    def keyPressEvent(self, event):
        if event.modifiers() == Qt.ControlModifier and event.key() == Qt.Key_S:  # Ctrl-S保存
            logger.info('Ctrl + S')
            self.saveAnnt()
        elif event.key() == Qt.Key_D: # 设置标注点
            self.act = Act.Annotate
            self.shape = Shape.Point
        elif event.key() == Qt.Key_C: # 设置标注圆形
            self.act = Act.Annotate
            self.shape = Shape.Circle
        elif event.key() == Qt.Key_R: # 设置标注矩形
            self.act = Act.Annotate
            self.shape = Shape.Rect
        elif event.key() == Qt.Key_P: # 设置标注多边形
            self.act = Act.Annotate
            self.shape = Shape.Polygon
        elif event.key() == Qt.Key_Delete: # 删除选中的item
            item = self.scene_.focusItem()
            if item.annt_type == self.curr_annt_type:
                self.delAnntByUUID(self.curr_annt_type, \
                        self.dct_annts[self.curr_annt_type], item.uuid)
                self.scene_.removeItem(item)
            logger.info('del focus item: {}, {}, {}'.format(item.annt_type, item.uuid, item))
        elif event.key() == Qt.Key_A: # 打印信息
            logger.info('curr_annt_type: {}'.format(self.curr_annt_type))
            logger.info('dct_annts: {}'.format(self.dct_annts))
            logger.info('items: {}'.format(['({}, {}, {})'.format(item.annt_type, item.uuid, item) for item in self.scene_.items()]))
        else:
            event.ignore()

    def addAnnt_(self, annt_type, dct_annt, annt, shape):
        UUID = None
        if annt_type == 'Tag':
            if dct_annt.get('tags') is None:
                logger.info('dct_annt have no tags')
                dct_annt['tags'] = {}

            if shape == Shape.Point:
                if dct_annt['tags'].get('add_point') is None:
                    dct_annt['tags']['add_point'] = []
                # 一定范围内有点则不允许再添加
                x0, y0 = annt[0] / self.size_scale_val, annt[1] / self.size_scale_val
                for pnt in dct_annt['tags']['add_point']:
                    x1, y1 = pnt['point']['x'], pnt['point']['y']
                    if ((x1-x0)**2 + (y1-y0)**2)**0.5 < 5:
                        break
                else:
                    UUID = str(uuid.uuid1())
                    a = {'id': UUID, 'point': {'x': x0, 'y': y0}}
                    dct_annt['tags']['add_point'].append(a)

            elif shape == Shape.UnPoint:
                if dct_annt['tags'].get('del_point') is None:
                    dct_annt['tags']['del_point'] = []
                UUID = str(uuid.uuid1())
                a = {'id': UUID, 'uuid': annt}
                dct_annt['tags']['del_point'].append(a)

            elif shape == Shape.Polygon:
                if dct_annt['tags'].get('del_area') is None:
                    dct_annt['tags']['del_area'] = []
                UUID = str(uuid.uuid1())
                a = {'id': UUID, 'polygon': [{'x': x / self.size_scale_val, 'y': y / self.size_scale_val} for x, y in annt]}
                dct_annt['tags']['del_area'].append(a)

            else: logger.error('{} is not currently supported'.format(shape))
        elif annt_type == 'ZJ':
            if dct_annt.get('Pathology') is None:
                logger.info('dct_annt have no Pathology')
                dct_annt['Pathology'] = []

            if shape == Shape.Polygon:
                UUID = str(uuid.uuid1())
                annt = ['1', 0, 0, 0, 0, 0.5, 0.5, 2, annt, {'UUID': UUID, 'Label': ''}]
                dct_annt['Pathology'].append(annt)
                # a = {'id': str(uuid.uuid1()), 'point': {'x': annt[0], 'y': annt[1]}}
                # dct_annt['tags']['add_point'].append(a)
            else: logger.error('{} is not currently supported'.format(shape))
        elif annt_type == 'COCO':
            logger.error('{} is not currently supported'.format(annt_type))
            return None
        else:
            logger.error('{} is undefined'.format(annt_type))
            return None
        return UUID

    def addAnnt(self, annt, shape, item=None):
        UUID = self.addAnnt_(self.curr_annt_type, self.dct_annts[self.curr_annt_type], annt, shape)
        if item is not None:
            item.annt_type = self.curr_annt_type
            item.uuid = UUID
        self.saveAnnt()
        self.modifyAnntSignle.emit()

    def addPoint(self, x, y, color=Color):
        ofs_x, ofs_y = 7 * self.img_scale_val, 13 * self.img_scale_val
        x -= ofs_x * self.size_scale_val
        y -= ofs_y * self.size_scale_val

        point = None
        x0, y0 = x * self.img_scale_val, y * self.img_scale_val
        for item in self.scene_.items():
            if isinstance(item, GraphicsSTextItem):
                x1, y1 = item.point
                if ((x1-x0)**2 + (y1-y0)**2)**0.5 < 5:
                    break
        else:
            point = GraphicsSTextItem(x, y, text='x', scale=self.img_scale_val, color=color, isEnableMove=self.isEnableMove)
            self.scene_.addItem(point)
        return point

    def addPolygon(self, paths, color=Color):
        polygon = GraphicsPathItem(scale=self.img_scale_val, color=color, isEnableMove=self.isEnableMove)
        for i, (x, y) in enumerate(paths):
            if i == 0:
                polygon.bgnPoint(x, y)
            elif i == len(paths) - 1:
                polygon.endPoint(x, y)
            else:
                polygon.addPoint(x, y)
        self.scene_.addItem(polygon)
        return polygon

    def mousePressEvent(self, event):
        super().mousePressEvent(event)
        if event.button() == Qt.LeftButton:   # 判断鼠标左键点击
            if self.isScrollDrag: pass
            else:
                self.isLMousePress = True
                if self.img_item is None: return

                # logger.info('mousePressEvent pos: {}'.format(event.pos()))
                # logger.info('act: {}, shape: {}'.format(self.act.name, self.shape.name))

                bgn_p = self.mapToScene(int(self.img_item.pos().x()), int(self.img_item.pos().y()))
                x, y = event.pos().x(), event.pos().y()
                x, y = x / self.scale_val + bgn_p.x(), y / self.scale_val + bgn_p.y()
                x, y = int(x), int(y)
                color = DCT_COLOR.get(self.curr_annt_type, Color)

                if self.act == Act.Invalid: pass
                elif self.act == Act.Annotate: # 标注
                    if self.shape == Shape.Point:
                        item = self.addPoint(x / self.size_scale_val, y / self.size_scale_val, color=color)
                        if item is not None:
                            self.addAnnt((x, y), self.shape, item)
                    elif self.shape == Shape.Circle:
                        pass
                    elif self.shape == Shape.Rect:
                        pass
                    elif self.shape == Shape.Polygon:
                        self.setDragMode(QGraphicsView.NoDrag)
                        self.currItem = GraphicsPathItem(scale=self.img_scale_val, color=color, isEnableMove=self.isEnableMove)
                        self.currItem.bgnPoint(x / self.size_scale_val, y / self.size_scale_val)
                        self.currPoses = [(x, y)]
                        self.scene_.addItem(self.currItem)
                    elif self.shape == Shape.UnPoint:
                        item = self.scene_.focusItem()
                        if item and item.annt_type != 'Tag':
                            self.addAnnt(item.uuid, self.shape)
                            self.updateAnnt(self.annt_type, self.dct_annts[self.annt_type])
                elif self.act == Act.Measure: # 测量
                    pass
        elif event.button() == Qt.RightButton:   # 判断鼠标右键点击
            self.isRMousePress = True
            self.isScrollDrag = True
            self.setDragMode(QGraphicsView.ScrollHandDrag)
            self.setInteractive(False)
            # 模拟鼠标左键
            event0 = QMouseEvent(QEvent.MouseButtonPress, event.pos(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
            self.mousePressEvent(event0)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        if self.isLMousePress:
            if self.img_item is None: return

            bgn_p = self.mapToScene(int(self.img_item.pos().x()), int(self.img_item.pos().y()))
            x, y = event.pos().x(), event.pos().y()
            x, y = x / self.scale_val + bgn_p.x(), y / self.scale_val + bgn_p.y()
            x, y = int(x), int(y)

            isUpdate = False
            now = datetime.datetime.now()
            if self.lt_time is None: isUpdate = True
            elif (now - self.lt_time).total_seconds() > self.refreshTime:
                isUpdate = True # 间隔足够长
            if isUpdate: self.lt_time = now

            if self.act == Act.Invalid: pass
            elif self.act == Act.Annotate: # 标注
                if self.shape == Shape.Point:
                    pass
                elif self.shape == Shape.Circle:
                    pass
                elif self.shape == Shape.Rect:
                    pass
                elif self.shape == Shape.Polygon:
                    if isUpdate:
                        self.currItem.addPoint(x / self.size_scale_val, y / self.size_scale_val)
                        self.currPoses.append((x, y))
            elif self.act == Act.Measure: # 测量
                pass
        elif self.isRMousePress: pass

    def mouseReleaseEvent(self, event):
        super().mouseReleaseEvent(event)
        if event.button() == Qt.LeftButton:
            self.isLMousePress = False
            if self.img_item is None: return

            bgn_p = self.mapToScene(int(self.img_item.pos().x()), int(self.img_item.pos().y()))
            x, y = event.pos().x(), event.pos().y()
            x, y = x / self.scale_val + bgn_p.x(), y / self.scale_val + bgn_p.y()
            x, y = int(x), int(y)

            if self.act == Act.Invalid: pass
            elif self.act == Act.Annotate: # 标注
                if self.shape == Shape.Point:
                    pass
                elif self.shape == Shape.Circle:
                    pass
                elif self.shape == Shape.Rect:
                    pass
                elif self.shape == Shape.Polygon:
                    self.currItem.endPoint(x / self.size_scale_val, y / self.size_scale_val)
                    self.currPoses.append((x, y))
                    if len(self.currPoses) > 3:
                        self.addAnnt(self.currPoses, self.shape, self.currItem)
                        self.updateAnnt(self.annt_type, self.dct_annts[self.annt_type])
                    else: self.scene_.removeItem(self.currItem)
                    self.currItem = None
                    self.currPoses = None
            elif self.act == Act.Measure: # 测量
                pass
            self.setDragMode(self.currDragMode)
        elif event.button() == Qt.RightButton:
            self.isRMousePress = False
            self.isScrollDrag = False
            self.setDragMode(GraphicsView.NoDrag)
            self.setDragMode(self.currDragMode)
            self.setInteractive(True)

    def setAct(self, act):
        if isinstance(act, Act):
            self.act = act

    def getAct(self):
        return self.act

    def setShape(self, shape):
        if isinstance(shape, Shape):
            self.shape = shape

    def getShape(self):
        return self.shape

    def closeEvent(self, event):
        if self.isExitSave:
            self.saveAnnt()

class Viewer(QWidget):
    def __init__(self, slideType=None):
        super().__init__()
        self.cfg = loadCfg()
        self.slideType = slideType
        self.act = self.cfg.get('act')
        self.shape = self.cfg.get('shape')
        self.annt_type = self.cfg.get('annt_type')
        self.img_dir = self.cfg.get('img_dir')
        self.img_idx = self.cfg.get('img_idx', 0)
        self.lst_img_file = []
        self.img_file = self.cfg.get('img_file')
        self.lst_btn_annotate = []
        self.lst_btn_measure = []

        self.setFocusPolicy(Qt.StrongFocus)
        self.view = GraphicsView(self, relSize=(800, 800))
        self.initUI()
        self.setSlot()
        self.updateDir()

        self.setWindowTitle('Graphics Viewer')
        self.resize(QSize(1080, 880))

    def initUI(self):
        lytMain = QHBoxLayout(self)
        left = QVBoxLayout()

        self.grpBase = QGroupBox('基础功能')
        self.grpBase.setFixedWidth(220)
        vbox = QVBoxLayout()

        line = QHBoxLayout()
        line.addStretch(0)
        line.addWidget(QLabel('标注格式'))
        self.cbAnntType = QComboBox()
        self.cbAnntType.addItems(['Tag', 'ZJ', 'Labelme', 'COCO'])
        self.cbAnntType.setCurrentText(self.annt_type)
        line.addWidget(self.cbAnntType)
        vbox.addLayout(line)

        line = QHBoxLayout()
        self.txtImgDir = QLineEdit('imgae dir')
        # self.txtImgDir.setFixedWidth(180)
        line.addWidget(self.txtImgDir)
        self.btnSetDir = QPushButton('...')
        self.btnSetDir.setFixedWidth(30)
        line.addWidget(self.btnSetDir)
        vbox.addLayout(line)

        line = QHBoxLayout()
        self.btnPrev = QPushButton('⬅')
        self.btnNext = QPushButton('➡')
        line.addWidget(self.btnPrev)
        line.addWidget(self.btnNext)
        vbox.addLayout(line)
        self.grpBase.setLayout(vbox)
        left.addWidget(self.grpBase)

        self.grpAnnotate = QGroupBox('标注')
        self.grpAnnotate.setFixedWidth(220)
        self.btnPoint = QPushButton('点')
        self.btnPoint.setFixedWidth(30)
        self.btnPoint.isSelected = False
        self.btnCircle = QPushButton('圆')
        self.btnCircle.setFixedWidth(30)
        self.btnCircle.isSelected = False
        self.btnRect = QPushButton('矩')
        self.btnRect.setFixedWidth(30)
        self.btnRect.isSelected = False
        self.btnPolygon = QPushButton('多')
        self.btnPolygon.setFixedWidth(30)
        self.btnPolygon.isSelected = False
        self.lst_btn_annotate.append(self.btnPoint)
        self.lst_btn_annotate.append(self.btnCircle)
        self.lst_btn_annotate.append(self.btnRect)
        self.lst_btn_annotate.append(self.btnPolygon)
        line = QHBoxLayout()
        line.addWidget(self.btnPoint)
        line.addWidget(self.btnCircle)
        line.addWidget(self.btnRect)
        line.addWidget(self.btnPolygon)
        line.addStretch(0)
        self.grpAnnotate.setLayout(line)
        left.addWidget(self.grpAnnotate)

        self.grpMeasure = QGroupBox('测量')
        self.grpMeasure.setFixedWidth(220)
        self.btnMLine = QPushButton('线')
        self.btnMLine.setFixedWidth(30)
        self.btnMLine.isSelected = False
        self.btnMCircle = QPushButton('圆')
        self.btnMCircle.setFixedWidth(30)
        self.btnMCircle.isSelected = False
        self.btnMRect = QPushButton('矩')
        self.btnMRect.setFixedWidth(30)
        self.btnMRect.isSelected = False
        self.btnMPolygon = QPushButton('多')
        self.btnMPolygon.setFixedWidth(30)
        self.btnMPolygon.isSelected = False
        self.lst_btn_measure.append(self.btnMLine)
        self.lst_btn_measure.append(self.btnMCircle)
        self.lst_btn_measure.append(self.btnMRect)
        self.lst_btn_measure.append(self.btnMPolygon)
        line = QHBoxLayout()
        line.addWidget(self.btnMLine)
        line.addWidget(self.btnMCircle)
        line.addWidget(self.btnMRect)
        line.addWidget(self.btnMPolygon)
        line.addStretch(0)
        self.grpMeasure.setLayout(line)
        left.addWidget(self.grpMeasure)

        left.addStretch()
        lytMain.addLayout(left)

        right = QVBoxLayout()
        self.txtImgUri = QLineEdit('image file')
        right.addWidget(self.txtImgUri)
        right.addWidget(self.view)
        lytMain.addLayout(right)

    def resizeEvent(self, event):
        super().resizeEvent(event)
        self.view.autoAdapt()

    def setSlot(self):
        self.btnSetDir.clicked.connect(self.setDir)
        self.btnPrev.clicked.connect(self.prev)
        self.btnNext.clicked.connect(self.next)
        self.btnPoint.clicked.connect(lambda: self.setAnnotate(Shape.Point))
        self.btnCircle.clicked.connect(lambda: self.setAnnotate(Shape.Circle))
        self.btnRect.clicked.connect(lambda: self.setAnnotate(Shape.Rect))
        self.btnPolygon.clicked.connect(lambda: self.setAnnotate(Shape.Polygon))

        self.btnMLine.clicked.connect(lambda: self.setMeasure(Shape.Line))
        self.btnMCircle.clicked.connect(lambda: self.setMeasure(Shape.Circle))
        self.btnMRect.clicked.connect(lambda: self.setMeasure(Shape.Rect))
        self.btnMPolygon.clicked.connect(lambda: self.setMeasure(Shape.Polygon))

        self.cbAnntType.currentIndexChanged.connect(self.changeAnntType)

    def changeAnntType(self):
        self.annt_type = self.cbAnntType.currentText()
        self.updateImg()

    def setAnnotate(self, shape):
        dct_tmp = {
                Shape.Point: self.btnPoint,
                Shape.Circle: self.btnCircle,
                Shape.Rect: self.btnRect,
                Shape.Polygon: self.btnPolygon,
                }
        btn = dct_tmp.get(shape)
        if btn is None:
            logger.error('shape {} is invalid'.format(shape.name))
            return

        if self.view.getAct() == Act.Annotate \
                and self.view.getShape() == shape:
            btn.isSelected = False
            self.view.setAct(Act.Invalid)
            self.view.setShape(Shape.Invalid)
        else:
            for b in self.lst_btn_annotate + self.lst_btn_measure:
                b.isSelected = False
            btn.isSelected = True
            self.view.setAct(Act.Annotate)
            self.view.setShape(shape)
        self.refreshButton()

    def setMeasure(self, shape):
        dct_tmp = {
                Shape.Line: self.btnMLine,
                Shape.Circle: self.btnMCircle,
                Shape.Rect: self.btnMRect,
                Shape.Polygon: self.btnMPolygon,
                }
        btn = dct_tmp.get(shape)
        if btn is None:
            logger.error('shape {} is invalid'.format(shape.name))
            return

        if self.view.getAct() == Act.Measure \
                and self.view.getShape() == shape:
            btn.isSelected = False
            self.view.setAct(Act.Invalid)
            self.view.setShape(Shape.Invalid)
        else:
            for b in self.lst_btn_annotate + self.lst_btn_measure:
                b.isSelected = False
            btn.isSelected = True
            self.view.setAct(Act.Measure)
            self.view.setShape(shape)
        self.refreshButton()

    def refreshButton(self):
        for b in self.lst_btn_annotate + self.lst_btn_measure:
            if b.isSelected:
                b.setStyleSheet('QPushButton{background-color: #BBB;}')
            else:
                b.setStyleSheet('QPushButton{background-color: #EEE;}')

    def setDir(self):
        tmp = QFileDialog.getExistingDirectory(self, '选择图片目录', self.img_dir)
        if os.path.isdir(tmp):
            self.img_dir = tmp
            self.img_idx = 0
            self.updateDir()

    def updateDir(self):
        if os.path.isdir(self.img_dir):
            self.txtImgDir.setText(self.img_dir)
            self.lst_img_file = sorted(get_imgs(self.img_dir))
            self.img_file = self.lst_img_file[self.img_idx]
            self.btnPrev.setEnabled(True)
            self.btnNext.setEnabled(True)
            self.updateImg()

    def updateImg(self):
        if len(self.lst_img_file) > 0:
            if len(self.lst_img_file) > self.img_idx >= 0:
                self.img_file = self.lst_img_file[self.img_idx]

        if os.path.isfile(self.img_file):
            self.txtImgUri.setText(self.img_file)
            if self.slideType is None:
                self.view.clearSlide()
                self.view.loadImg(self.img_file)
                if self.annt_type == 'Tag':
                    self.view.loadAnnt(self.img_file[:-4] + '_tag.json', self.annt_type)
                elif self.annt_type == 'ZJ':
                    self.view.loadAnnt(self.img_file[:-4] + '.json', self.annt_type)
                else: logger.error('{} is not currently supported'.format(self.annt_type))
            else: pass

    def prev(self):
        if self.img_idx > 0:
            self.img_idx -= 1
            self.updateImg()
            self.btnNext.setEnabled(True)
        else: self.btnPrev.setEnabled(False)

    def next(self):
        if self.img_idx < len(self.lst_img_file) - 1:
            self.img_idx += 1
            self.updateImg()
            self.btnPrev.setEnabled(True)
        else: self.btnNext.setEnabled(False)

    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Q:
            self.close()
        elif event.key() == Qt.Key_Left:
            self.prev()
        elif event.key() == Qt.Key_Right:
            self.next()

    def closeEvent(self, event):
        self.view.close()
        # self.cfg['act'] = self.act
        # self.cfg['shape'] = self.shape
        self.cfg['annt_type'] = self.annt_type
        self.cfg['img_dir'] = self.img_dir
        self.cfg['img_idx'] = self.img_idx
        self.cfg['img_file'] = self.img_file
        saveCfg(self.cfg)
        super().closeEvent(event)

def loadCfg():
    if os.path.isfile('cfg.json'):
        with open('cfg.json', 'r') as fp:
            cfg = json.load(fp)
    else:
        cfg = {
                'annt_type': 'Tag',
                'act': None,
                'shape': None,
                'img_dir': '',
                'img_file': '',
                'img_idx': 0
                }
    return cfg

def saveCfg(cfg):
    with open('cfg.json', 'w') as fp:
        json.dump(cfg, fp, indent=2)


if __name__=='__main__':
    parser = argparse.ArgumentParser()
    args = parser.parse_args()

    QCoreApplication(sys.argv)
    QCoreApplication.setAttribute(Qt.AA_EnableHighDpiScaling)

    app = QApplication(sys.argv)
    # win = Viewer('ELISPOT')
    win = Viewer()
    win.show()
    sys.exit(app.exec_())
